<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>CubeCamera Rainbow — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="module">
        import { AdditiveBlending, BackSide, BlurMaterial, Color, ColorManagement, CubeCamera, GLSL3, Group, IcosahedronGeometry, ImageBitmapLoaderThread, LinearMipmapLinearFilter, LinearSRGBColorSpace, MathUtils, Mesh, MeshBasicMaterial, NoBlending, OrthographicCamera, PanelItem, PerspectiveCamera, RawShaderMaterial, Scene, SphereGeometry, Sprite, SpriteMaterial, TextureLoader, UI, Vector2, Vector3, WebGLCubeRenderTarget, WebGLRenderTarget, WebGLRenderer, Wobble, getFullscreenTriangle, getKeyByValue, ticker, tween } from '../../build/alien.three.js';

        const isDebug = /[?&]debug/.test(location.search);

        const colors = {
            backgroundColor: 0x030627,
            lightColor: 0xff8dfc,
            particleColor: 0x8080ff
        };

        // https://twitter.com/pschroen/status/1405600020284444674

        // Based on https://github.com/yuichiroharai/glsl-y-hsv
        // Based on https://github.com/spite/codevember-2016

        import hsv2rgbSmooth from '../../src/shaders/modules/hsv/hsv2rgbSmooth.glsl.js';
        import rgb2hsv from '../../src/shaders/modules/hsv/rgb2hsv.glsl.js';
        import noise from '../../src/shaders/modules/noise/noise2d.glsl.js';
        import rgbshift from '../../src/shaders/modules/rgbshift/rgbshift.glsl.js';
        import blendOverlay from '../../src/shaders/modules/blending/overlay.glsl.js';
        import blendSoftLight from '../../src/shaders/modules/blending/soft-light.glsl.js';

        class CompositeMaterial extends RawShaderMaterial {
            constructor() {
                const { getTexture, scene } = WorldController;

                super({
                    glslVersion: GLSL3,
                    uniforms: {
                        tScene: { value: null },
                        tBloom: { value: null },
                        tLensDirt: { value: getTexture('lens_dirt.jpg') },
                        uLightColor: { value: new Color(colors.lightColor) },
                        uColor: { value: scene.background },
                        uRGBAmount: { value: 0.2 },
                        uBloomReduction: { value: 0.07 },
                        uBloomBoost: { value: 0.4 },
                        uBloomClamp: { value: 1 },
                        uHue: { value: 0 },
                        uReduction: { value: 1.2 },
                        uBoost: { value: 1 },
                        uLensDirt: { value: true },
                        uDebug: { value: isDebug },
                        uResolution: { value: new Vector2() },
                        uTime: { value: 0 }
                    },
                    vertexShader: /* glsl */ `
                        in vec3 position;
                        in vec2 uv;

                        out vec2 vUv;

                        void main() {
                            vUv = uv;

                            gl_Position = vec4(position, 1.0);
                        }
                    `,
                    fragmentShader: /* glsl */ `
                        precision highp float;

                        uniform sampler2D tScene;
                        uniform sampler2D tBloom;
                        uniform sampler2D tLensDirt;
                        uniform vec3 uLightColor;
                        uniform vec3 uColor;
                        uniform float uRGBAmount;
                        uniform float uBloomReduction;
                        uniform float uBloomBoost;
                        uniform float uBloomClamp;
                        uniform float uHue;
                        uniform float uReduction;
                        uniform float uBoost;
                        uniform bool uLensDirt;
                        uniform bool uDebug;
                        uniform vec2 uResolution;
                        uniform float uTime;

                        in vec2 vUv;

                        out vec4 FragColor;

                        ${hsv2rgbSmooth}
                        ${rgb2hsv}
                        ${noise}
                        ${rgbshift}
                        ${blendOverlay}
                        ${blendSoftLight}

                        void main() {
                            float center = length(vUv - 0.5);

                            vec3 rgb = uColor;
                            vec3 hsv = rgb2hsv(rgb);
                            hsv.x += vUv.x + uHue;

                            rgb = hsv2rgbSmooth(hsv);

                            // Linear gradient
                            vec4 color = vec4(mix(rgb, vec3(0.0), smoothstep(0.0, 0.6, vUv.y)), 1.0);

                            color += getRGB(tScene, vUv, 0.1, 0.002 * uRGBAmount);

                            vec4 bloom = texture(tBloom, vUv);
                            bloom.r = max(0.0, bloom.r - uBloomReduction);
                            bloom.g = max(0.0, bloom.g - uBloomReduction);
                            bloom.b = max(0.0, bloom.b - uBloomReduction);
                            bloom.rgb *= uBloomBoost;
                            bloom = clamp(bloom, 0.0, uBloomClamp);

                            color.rgb += bloom.rgb;

                            // Blend overlay noise lighting
                            float noise = noise(vUv + uTime * 0.06);
                            float amount = smoothstep(0.2, 0.7, center) * noise;

                            if (uDebug) {
                                color += vec4(vec3(1.0, 0.0, 0.0), 1.0) * amount;
                            } else {
                                color = blendOverlay(color, vec4(uLightColor, 1.0), amount);
                            }

                            // Blend overlay vignetting
                            float vignette = uBoost - center * uReduction;

                            vec4 base = color;
                            vec4 blend = vec4(vec3(vignette), 1.0);

                            color = blendOverlay(base, blend, 1.0);

                            // Blend soft light background color
                            base = color;
                            blend = vec4(rgb, 1.0);

                            color = blendSoftLight(base, blend, 0.8);

                            // Dirt lens texture
                            if (uLensDirt) {
                                vec2 vUv2 = vUv;

                                float aspectRatio2 = 1.0;
                                float aspectRatio = uResolution.x / uResolution.y;

                                if (aspectRatio2 > aspectRatio) {
                                    float widthRatio = aspectRatio / aspectRatio2;
                                    vUv2.x = vUv.x * widthRatio;
                                    vUv2.x += 0.5 * (1.0 - widthRatio);
                                    vUv2.y = vUv.y;
                                } else {
                                    float heightRatio = aspectRatio2 / aspectRatio;
                                    vUv2.x = vUv.x;
                                    vUv2.y = vUv.y * heightRatio;
                                    vUv2.y += 0.5 * (1.0 - heightRatio);
                                }

                                color.rgb += smoothstep(0.0, 0.4, bloom.rgb) * texture(tLensDirt, vUv2).rgb;
                            }

                            FragColor = color;
                        }
                    `,
                    blending: NoBlending,
                    depthTest: false,
                    depthWrite: false
                });
            }
        }

        class Sphere extends Group {
            constructor() {
                super();

                this.initMesh();
            }

            initMesh() {
                const { cubeRenderTarget } = WorldController;

                const geometry = new SphereGeometry(1, 80, 80);

                // Chrome material
                const material = new MeshBasicMaterial({
                    envMap: cubeRenderTarget.texture
                });

                const mesh = new Mesh(geometry, material);
                this.add(mesh);
            }
        }

        class Stars extends Group {
            constructor(numParticles) {
                super();

                this.numParticles = numParticles;

                this.sprites = [];
                this.nearScale = 0.3;

                this.initSprites();
            }

            initSprites() {
                const { getTexture, hue, resolution } = WorldController;

                const material = new SpriteMaterial({
                    map: getTexture('particle2.png'),
                    color: new Color(colors.particleColor),
                    transparent: true,
                    blending: AdditiveBlending,
                    depthWrite: false
                });

                material.onBeforeCompile = shader => {
                    shader.uniforms.hue = hue;
                    shader.uniforms.resolution = resolution;

                    shader.fragmentShader = shader.fragmentShader.replace(
                        'void main() {',
                        /* glsl */ `
                        uniform float hue;
                        uniform vec2 resolution;

                        ${hsv2rgbSmooth}
                        ${rgb2hsv}

                        void main() {
                        `
                    );

                    shader.fragmentShader = shader.fragmentShader.replace(
                        'outgoingLight = diffuseColor.rgb;',
                        /* glsl */ `
                        vec2 position = (gl_FragCoord.xy / resolution);

                        vec3 hsv = rgb2hsv(diffuseColor.rgb);
                        hsv.x += position.x + hue;

                        outgoingLight = hsv2rgbSmooth(hsv);
                        `
                    );
                };

                for (let i = 0; i < this.numParticles; i++) {
                    const distance = MathUtils.randFloat(10, 40);

                    const sprite = new Sprite(material);
                    sprite.position.x = Math.random() * 2 - 1;
                    sprite.position.y = Math.random() * 2 - 1;
                    sprite.position.z = Math.random() * 2 - 1;
                    sprite.position.normalize();
                    sprite.position.multiplyScalar(distance);
                    sprite.scale.set(0.3, 0.3, 1);

                    sprite.random = Math.random();

                    sprite.origin = new Vector3();
                    sprite.near = new Vector3();
                    sprite.origin.copy(sprite.position);
                    sprite.near.copy(sprite.origin).multiplyScalar(this.nearScale);

                    sprite.wobble = new Wobble(sprite.origin);
                    sprite.wobble.scale = 0.3;

                    this.add(sprite);
                    this.sprites.push(sprite);
                }
            }

            // Public methods

            update = (time, near) => {
                this.sprites.forEach(sprite => {
                    if (near) {
                        sprite.wobble.origin.copy(sprite.wobble.position);
                        // sprite.wobble.update(sprite.random + time);
                        sprite.wobble.update(time * sprite.random);
                        sprite.position.copy(sprite.near.copy(sprite.origin).multiplyScalar(this.nearScale));
                    } else {
                        sprite.position.copy(sprite.origin);
                    }
                });
            };
        }

        class Space extends Group {
            constructor() {
                super();
            }

            async initMesh() {
                const { loadTexture } = WorldController;

                const geometry = new IcosahedronGeometry(40, 3);

                const map = await loadTexture('space.jpg');

                const material = new MeshBasicMaterial({
                    map,
                    side: BackSide,
                    transparent: true,
                    opacity: 0.05
                });

                const mesh = new Mesh(geometry, material);
                this.add(mesh);
            }

            // Public methods

            ready = () => this.initMesh();
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.visible = false;

                this.initRenderer();
                this.initViews();
            }

            initRenderer() {
                const { renderer, scene, cubeCamera } = WorldController;

                this.renderer = renderer;
                this.scene = scene;
                this.cubeCamera = cubeCamera;
            }

            initViews() {
                this.space = new Space();
                this.add(this.space);

                this.stars = new Stars(2000);
                this.add(this.stars);

                this.sphere = new Sphere();
                this.add(this.sphere);
            }

            // Public methods

            update = time => {
                this.sphere.visible = false;
                this.stars.update(time, true);
                this.cubeCamera.update(this.renderer, this.scene);
                this.stars.update(time, false);
                this.sphere.visible = true;
            };

            ready = () => this.space.ready();
        }

        class SceneController {
            static init(view) {
                this.view = view;
            }

            // Public methods

            static resize = () => {
            };

            static update = time => {
                this.view.update(time);
            };

            static animateIn = () => {
                this.view.visible = true;
            };

            static ready = () => this.view.ready();
        }

        class PanelController {
            static init(ui) {
                this.ui = ui;

                this.initPanel();
            }

            static initPanel() {
                const { hBlurMaterial, vBlurMaterial, compositeMaterial } = RenderManager;

                const postOptions = {
                    Off: false,
                    Post: true
                };

                const debugOptions = {
                    Off: false,
                    Debug: true
                };

                const dirtOptions = {
                    Off: false,
                    Dirt: true
                };

                const items = [
                    {
                        name: 'FPS'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'list',
                        list: postOptions,
                        value: getKeyByValue(postOptions, RenderManager.enabled),
                        callback: value => {
                            RenderManager.enabled = postOptions[value];
                        }
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        name: 'Chroma',
                        min: 0,
                        max: 10,
                        step: 0.1,
                        value: compositeMaterial.uniforms.uRGBAmount.value,
                        callback: value => {
                            compositeMaterial.uniforms.uRGBAmount.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Reduce',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: compositeMaterial.uniforms.uReduction.value,
                        callback: value => {
                            compositeMaterial.uniforms.uReduction.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Boost',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: compositeMaterial.uniforms.uBoost.value,
                        callback: value => {
                            compositeMaterial.uniforms.uBoost.value = value;
                        }
                    },
                    {
                        type: 'list',
                        list: debugOptions,
                        value: getKeyByValue(debugOptions, compositeMaterial.uniforms.uDebug.value),
                        callback: value => {
                            compositeMaterial.uniforms.uDebug.value = debugOptions[value];
                        }
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        name: 'Blur',
                        min: 0,
                        max: 10,
                        step: 0.1,
                        value: hBlurMaterial.uniforms.uBlurAmount.value,
                        callback: value => {
                            hBlurMaterial.uniforms.uBlurAmount.value = value;
                            vBlurMaterial.uniforms.uBlurAmount.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Reduce',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: compositeMaterial.uniforms.uBloomReduction.value,
                        callback: value => {
                            compositeMaterial.uniforms.uBloomReduction.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Boost',
                        min: 0,
                        max: 10,
                        step: 0.1,
                        value: compositeMaterial.uniforms.uBloomBoost.value,
                        callback: value => {
                            compositeMaterial.uniforms.uBloomBoost.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Clamp',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: compositeMaterial.uniforms.uBloomClamp.value,
                        callback: value => {
                            compositeMaterial.uniforms.uBloomClamp.value = value;
                        }
                    },
                    {
                        type: 'list',
                        list: dirtOptions,
                        value: getKeyByValue(dirtOptions, compositeMaterial.uniforms.uLensDirt.value),
                        callback: value => {
                            compositeMaterial.uniforms.uLensDirt.value = dirtOptions[value];
                        }
                    }
                ];

                items.forEach(data => {
                    this.ui.addPanel(new PanelItem(data));
                });
            }
        }

        const BlurDirectionX = new Vector2(1, 0);
        const BlurDirectionY = new Vector2(0, 1);

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;

                this.blurResolutionScale = 0.25;
                this.blurAmount = 1.5;
                this.enabled = true;

                this.initRenderer();
            }

            static initRenderer() {
                const { screenTriangle, hue, resolution, time } = WorldController;

                // Fullscreen triangle
                this.screenCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
                this.screen = new Mesh(screenTriangle);
                this.screen.frustumCulled = false;

                // Render targets
                this.renderTarget = new WebGLRenderTarget(1, 1, {
                    depthBuffer: false
                });

                this.renderTargetBlurA = this.renderTarget.clone();
                this.renderTargetBlurB = this.renderTarget.clone();

                this.renderTarget.depthBuffer = true;

                // Gaussian blur materials
                this.hBlurMaterial = new BlurMaterial(BlurDirectionX);
                this.hBlurMaterial.uniforms.uBlurAmount.value = this.blurAmount;

                this.vBlurMaterial = new BlurMaterial(BlurDirectionY);
                this.vBlurMaterial.uniforms.uBlurAmount.value = this.blurAmount;

                // Composite material
                this.compositeMaterial = new CompositeMaterial();
                this.compositeMaterial.uniforms.uHue = hue;
                this.compositeMaterial.uniforms.uResolution = resolution;
                this.compositeMaterial.uniforms.uTime = time;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.renderTarget.setSize(width, height);

                width = Math.round(width * this.blurResolutionScale);
                height = Math.round(height * this.blurResolutionScale);

                this.renderTargetBlurA.setSize(width, height);
                this.renderTargetBlurB.setSize(width, height);

                this.hBlurMaterial.uniforms.uResolution.value.set(width, height);
                this.vBlurMaterial.uniforms.uResolution.value.set(width, height);
            };

            static update = () => {
                const renderer = this.renderer;
                const scene = this.scene;
                const camera = this.camera;

                if (!this.enabled) {
                    renderer.setRenderTarget(null);
                    renderer.render(scene, camera);
                    return;
                }

                const renderTarget = this.renderTarget;
                const renderTargetBlurA = this.renderTargetBlurA;
                const renderTargetBlurB = this.renderTargetBlurB;

                // Scene pass
                renderer.setRenderTarget(renderTarget);
                renderer.render(scene, camera);

                // Two pass Gaussian blur (horizontal and vertical)
                this.hBlurMaterial.uniforms.tMap.value = renderTarget.texture;
                this.screen.material = this.hBlurMaterial;
                renderer.setRenderTarget(renderTargetBlurA);
                renderer.render(this.screen, this.screenCamera);

                this.vBlurMaterial.uniforms.tMap.value = renderTargetBlurA.texture;
                this.screen.material = this.vBlurMaterial;
                renderer.setRenderTarget(renderTargetBlurB);
                renderer.render(this.screen, this.screenCamera);

                // Composite pass (render to screen)
                this.compositeMaterial.uniforms.tScene.value = renderTarget.texture;
                this.compositeMaterial.uniforms.tBloom.value = renderTargetBlurB.texture;
                this.screen.material = this.compositeMaterial;
                renderer.setRenderTarget(null);
                renderer.render(this.screen, this.screenCamera);
            };
        }

        class CameraController {
            static init(camera) {
                this.camera = camera;

                this.mouse = new Vector2();
                this.target = new Vector2();

                // Motion control
                this.group = new Group();
                this.innerGroup = new Group();
                this.group.add(this.innerGroup);
                this.group.matrixAutoUpdate = false;
                this.innerGroup.matrixAutoUpdate = false;

                // Start position
                this.innerGroup.position.copy(this.camera.position);

                this.rotation = 0.0002;
                this.lerpSpeed = 0.05;
                this.multiplier = 1;
                this.enabled = false;
                this.prevent = true;

                this.addListeners();
            }

            static addListeners() {
                window.addEventListener('pointermove', this.onPointerMove);
            }

            // Event handlers

            static onPointerMove = ({ clientX, clientY }) => {
                if (this.prevent) {
                    return;
                }

                this.mouse.x = clientX - this.halfWidth;
                this.mouse.y = clientY - this.halfHeight;

                this.target.x = MathUtils.degToRad(-360) + this.mouse.x * this.rotation * this.multiplier;
                this.target.y = this.mouse.y * this.rotation * this.multiplier;
            };

            // Public methods

            static resize = (width, height) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                this.halfWidth = width / 2;
                this.halfHeight = height / 2;

                if (width < height) {
                    this.camera.position.z = 10;
                    this.multiplier = 2;
                } else {
                    this.camera.position.z = 8;
                    this.multiplier = 1;
                }

                this.innerGroup.position.z = this.camera.position.z;
            };

            static update = () => {
                if (!this.enabled) {
                    return;
                }

                this.group.rotation.x += (this.target.y - this.group.rotation.x) * this.lerpSpeed;
                this.group.rotation.y += (this.target.x - this.group.rotation.y) * this.lerpSpeed;

                this.updateCamera();
            };

            static updateCamera = () => {
                this.group.updateMatrix();
                this.innerGroup.updateMatrix();
                this.group.updateMatrixWorld();
                this.innerGroup.matrixWorld.decompose(this.camera.position, this.camera.quaternion, this.camera.scale);
            };

            static animateIn = () => {
                this.enabled = true;

                tween(this.target, { x: MathUtils.degToRad(-360) }, 4200, 'easeInOutQuart', () => {
                    this.prevent = false;
                });
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initLights();
                this.initLoaders();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(colors.backgroundColor);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 50;
                this.camera.position.z = 8;
                this.camera.zoom = 0.8;
                this.camera.lookAt(this.scene.position);

                // Cube render target
                this.cubeRenderTarget = new WebGLCubeRenderTarget(512, {
                    minFilter: LinearMipmapLinearFilter,
                    generateMipmaps: true
                });

                // Cube camera
                this.cubeCamera = new CubeCamera(this.camera.near, this.camera.far, this.cubeRenderTarget);
                this.scene.add(this.cubeCamera);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Initial background color
                this.color = {};
                this.scene.background.getHSL(this.color);

                // Global uniforms
                this.hue = { value: this.color.h };
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };
            }

            static initLights() {
            }

            static initLoaders() {
                this.textureLoader = new TextureLoader();
                this.textureLoader.setPath('../assets/textures/');
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.hue.value += 0.0005;

                this.scene.background.setHSL(this.hue.value, this.color.s, this.color.l);

                this.time.value = time;
                this.frame.value = frame;
            };

            // Global handlers

            static getTexture = (path, callback) => this.textureLoader.load(path, callback);

            static loadTexture = path => this.textureLoader.loadAsync(path);
        }

        class App {
            static async init() {
                this.initThread();
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                await SceneController.ready();

                this.initPanel();

                CameraController.animateIn();
                SceneController.animateIn();
            }

            static initThread() {
                ImageBitmapLoaderThread.init();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);

                this.ui = new UI({ fps: true });
                this.ui.animateIn();
                document.body.appendChild(this.ui.element);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                CameraController.init(camera);
                SceneController.init(this.view);
                RenderManager.init(renderer, scene, camera);
            }

            static initPanel() {
                PanelController.init(this.ui);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                CameraController.resize(width, height);
                SceneController.resize();
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                CameraController.update();
                SceneController.update(time);
                RenderManager.update(time, delta, frame);
                this.ui.update();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
